package com.gomoku;

import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Point;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;

// 游戏
class Game extends Socket implements Runnable {
    // 回调
    public interface Callback {
        void onGameOver(GameOverReason reason);                     // 游戏结束事件
        void onGameStart(String rivalName);                         // 游戏开始事件
        void onGetFindUsersResult(FindUsersResult result);          // 得到寻找用户结果事件
        void onGetInviteRequest(String rivalName);                  // 得到挑战请求事件
        void onGetInviteResult(String rivalName, boolean accept);   // 得到挑战结果事件
        void onGetLogonResult(boolean succeeded);                   // 得到登录结果事件
        void onRivalQuitGame(boolean escape, String rivalName);     // 对手退出游戏事件
    }

    // 棋子类型
    public enum Piece {
        NOTHING,    // 什么也没有
        BLACK,      // 黑棋
        WHITE       // 白棋
    }

    // 状态
    public enum Status {
        PLAYING,        // 自己下棋中
        END,            // 结束
        RIVAL_PLAYING   // 对手下棋中
    }

    private final Callback mCallback;           // 回调
    private final Graphics mDrawer;             // 绘图者
    private final Component mGameBoard;         // 游戏区域
    private boolean mIsBlack;                   // 黑棋则为 true，白棋则为 false
    private final Point mLastPos;               // 上一次放置棋子的位置
    private Piece[][] mPieces;                  // 所有棋子
    private String mRivalName;                  // 对手的名字
    private final ObjectOutputStream mSender;   // 数据发送者
    private Status mStatus;                     // 状态
    private String mUsername;                   // 用户名
    
    /**
     * 构造方法。
     * @param callback  回调
     * @param drawer    绘图者
     * @param gameBoard 游戏区域
     */
    public Game(Callback callback, Graphics drawer, Component gameBoard) throws Exception {
        // 初始化成员
        mCallback = callback;
        mDrawer = drawer;
        mGameBoard = gameBoard;
        mIsBlack = true;
        mLastPos = new Point();
        mStatus = Status.END;

        // 重绘游戏区域
        this.drawGameBoard();
        
        // 连接服务器
        final InetAddress serverAddr = InetAddress.getByName(GomokuProtocol.SERVER_IP);
        super.connect(new InetSocketAddress(serverAddr, GomokuProtocol.SERVER_PORT));
        mSender = new ObjectOutputStream(super.getOutputStream());
        
        // 开启接收数据的线程
        new Thread(this).start();
    }
    
    /**
     * 接受或拒绝挑战。
     * @param rivalName 对手的名字
     * @param accept    接受挑战则为 true，否则为 false
     */
    public void acceptInvite(String rivalName, boolean accept) {
        // 如果接受则游戏开始
        if (accept) {
            mRivalName = rivalName;
            this.start(false);
        }

        // 发送结果
        final InviteResult result = new InviteResult(accept, mUsername, rivalName);
        this.sendData(result);
    }

    /**
     * 检查横向。
     * @return 胜利则返回 true，否则返回 false
     */
    private boolean checkHorizontal() {
        final Piece checkPiece = mIsBlack ? Piece.BLACK: Piece.WHITE;  // 要检查的棋子
        int count = 0;                                                  // 连续的棋子的数量
        
        // 从左到右检查一行
        for (int i = 0; i < Const.GAME_BOARD_POINT_COUNT_H; i++) {
            // 查找棋子
            if (mPieces[mLastPos.y][i] == checkPiece) {
                count++;
            } else {
                count = 0;
            }
            
            // 如果5个棋子连在一起就胜利
            if (count == Const.WIN_NUMBER) {
                return true;
            }
        }

        return false;
    }

    /**
     * 检查斜向（从左上到右下）。
     * @return 胜利则返回 true，否则返回 false
     */
    private boolean checkOblique1() {
        final Piece checkPiece = mIsBlack ? Piece.BLACK: Piece.WHITE;   // 要检查的棋子
        int count = 0;                                                  // 连续的棋子的数量
        final int min = Math.min(mLastPos.x, mLastPos.y);               // 范围坐标，用以确定左边界和上边界
        final int left = mLastPos.x - min;                              // 左边界
        final int top = mLastPos.y - min;                               // 上边界

        // 从左上到右下检查一斜行
        for (int i = left, j = top; i < Const.GAME_BOARD_POINT_COUNT_H && j < Const.GAME_BOARD_POINT_COUNT_V; i++, j++) {
            // 查找棋子
            if (mPieces[j][i] == checkPiece) {
                count++;
            } else {
                count = 0;
            }
            
            // 如果5个棋子连在一起就胜利
            if (count == Const.WIN_NUMBER) {
                return true;
            }
        }

        return false;
    }

    /**
     * 检查斜向（从左下到右上）。
     * @return 胜利则返回 true，否则返回 false
     */
    private boolean checkOblique2() {
        final Piece checkPiece = mIsBlack ? Piece.BLACK: Piece.WHITE;   // 要检查的棋子
        int count = 0;                                                  // 连续的棋子的数量
        int left = 0;                                                   // 左边界
        int bottom;                                                     // 下边界
        
        // 计算左边界和下边界
        if (mLastPos.x + mLastPos.y < Const.GAME_BOARD_POINT_COUNT_H) {
            bottom = mLastPos.x + mLastPos.y;
        } else {
            left = (mLastPos.x + mLastPos.y) - Const.GAME_BOARD_POINT_COUNT_H + 1;
            bottom = Const.GAME_BOARD_POINT_COUNT_V - 1;
        }

        // 从左下到右上检查一斜行
        for (int i = left, j = bottom; i < Const.GAME_BOARD_POINT_COUNT_H && j >= 0; i++, j--) {
            // 查找棋子
            if (mPieces[j][i] == checkPiece) {
                count++;
            } else {
                count = 0;
            }
            
            // 如果5个棋子连在一起就胜利
            if (count == Const.WIN_NUMBER) {
                return true;
            }
        }

        return false;
    }

    /**
     * 检查纵向。
     * @return 胜利则返回 true，否则返回 false
     */
    private boolean checkVertical() {
        final Piece checkPiece = mIsBlack ? Piece.BLACK: Piece.WHITE;  // 要检查的棋子
        int count = 0;                                                  // 连续的棋子的数量
        
        // 从上到下检查一列
        for (int j = 0; j < Const.GAME_BOARD_POINT_COUNT_V; j++) {
            // 查找棋子
            if (mPieces[j][mLastPos.x] == checkPiece) {
                count++;
            } else {
                count = 0;
            }
            
            // 如果5个棋子连在一起就胜利
            if (count == Const.WIN_NUMBER) {
                return true;
            }
        }

        return false;
    }
    
    /**
     * 关闭游戏。
     */
    @Override
    public void close() {
        // 发送退出游戏请求
        final QuitGame quitGame = new QuitGame(mUsername);
        this.sendData(quitGame);
        
        try {
            // 延迟一下，防止数据还没发送完成就关闭网络
            Thread.sleep(100);
            
            // 关闭网络
            super.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    /**
     * 认输。
     */
    public void defeat() {
        // 发送游戏结束数据
        final GameOver gameOver = new GameOver(GameOverReason.SELF_ADMIT, mRivalName);
        this.sendData(gameOver);
        
        // 游戏结束
        this.gameOver(GameOverReason.SELF_ADMIT);
    }

    /**
     * 画游戏区域。
     */
    private void drawGameBoard() {
        // 画背景色
        mDrawer.setColor(Const.GAME_BOARD_COLOR);
        mDrawer.fillRect(0, 0, Const.GAME_BOARD_WIDTH, Const.GAME_BOARD_HEIGHT);
        
        // 画线
        mDrawer.setColor(Const.GAME_BOARD_BORDER_COLOR);
        for (int i = 1; i <= Const.GAME_BOARD_POINT_COUNT_H; i++) {
            // 画横线
            mDrawer.drawLine(Const.GRID_SIZE, i * Const.GRID_SIZE, Const.GAME_BOARD_WIDTH - Const.GRID_SIZE, i * Const.GRID_SIZE);
            
            // 画竖线
            mDrawer.drawLine(i * Const.GRID_SIZE, Const.GRID_SIZE, i * Const.GRID_SIZE, Const.GAME_BOARD_HEIGHT - Const.GRID_SIZE);
        }
        
        // 通知游戏区域重绘
        mGameBoard.repaint();
    }
    
    /**
     * 寻找其它用户。
     */
    public void findUsers() {
        if (mUsername.isEmpty()) {
            return;
        }

        final FindUsersRequest request = new FindUsersRequest(mUsername);
        this.sendData(request);
    }
    
    /**
     * 游戏结束。
     * @param reason    结束原因
     */
    private void gameOver(GameOverReason reason) {
        // 将状态设置为已结束
        mStatus = Game.Status.END;

        // 发送通知
        mCallback.onGameOver(reason);
    }
    
    /**
     * 处理寻找其它用户结果。
     * @param result    结果
     */
    private void handleFindUsers(FindUsersResult result) {
        // 发送通知
        mCallback.onGetFindUsersResult(result);
    }

    /**
     * 处理游戏结束。
     * @param gameOver  游戏结束信息
     */
    private void handleGameOver(GameOver gameOver) {
        // 如果没在下棋中，则直接返回
        if (mStatus == Status.END) {
            return;
        }
        
        // 将状态设置为已结束
        mStatus = Status.END;
        
        // 发送通知
        GameOverReason reason = gameOver.mReason;
        if (reason == GameOverReason.WON) {
            reason = GameOverReason.LOST;
        } else if (reason == GameOverReason.SELF_ADMIT) {
            reason = GameOverReason.RIVAL_ADMIT;
        }
        mCallback.onGameOver(reason);
    }

    /**
     * 处理挑战请求。
     * @param request 请求
     */
    private void handleInviteRequest(InviteRequest request) {
        // 发送通知
        mCallback.onGetInviteRequest(request.mRivalName);
    }

    /**
     * 处理挑战结果。
     * @param result  结果
     */
    private void handleInviteResult(InviteResult result) {
        // 如果接受则游戏开始
        if (result.mAccept) {
            mRivalName = result.mRivalName;
            this.start(true);
        }
        
        // 发送通知
        mCallback.onGetInviteResult(result.mRivalName, result.mAccept);
    }
    
    /**
     * 处理登录结果。
     * @param result    结果
     */
    private void handleLogon(LogonResult result) {
        boolean succeeded = !result.mUsername.isEmpty();
        if (succeeded) {
            // 保存用户名
            mUsername = result.mUsername;
        }

        // 发送通知
        mCallback.onGetLogonResult(succeeded);
    }

    /**
     * 处理放置棋子。
     * @param putPiece  棋子信息
     */
    private void handlePutPiece(PutPiece putPiece) {
        // 放一个与自己颜色相反的棋子
        this.putPiece(!mIsBlack, putPiece.mX, putPiece.mY);
        
        // 将状态设置为游戏中
        mStatus = Status.PLAYING;
    }
    
    /**
     * 处理对手退出游戏事件。
     * @param quitGame  退出游戏信息
     */
    private void handleQuitGame(QuitGame quitGame) {
        // 如果不是当前对手，则直接返回
        if (!mRivalName.equals(quitGame.mUsername)) {
            return;
        }
        
        // 发送通知
        boolean isPlaying = mStatus != Status.END;
        mCallback.onRivalQuitGame(isPlaying, quitGame.mUsername);
        
        // 对手名置空
        mRivalName = "";
        
        // 将状态设置为已结束
        mStatus = Status.END;
    }
    
    /**
     * 发起挑战。
     * @param rivalName 对手的名字
     */
    public void invite(String rivalName) {
        if (rivalName.isEmpty()) {
            return;
        }

        // 发送挑战请求
        final InviteRequest request = new InviteRequest(mUsername, rivalName);
        this.sendData(request);
    }

    /**
     * 判断是否胜利。
     * @return 胜利则返回 true，否则返回 false
     */
    private boolean isWon() {
        // 检查横向
        boolean result = this.checkHorizontal();
        if (result) {
            return true;
        }
        
        // 检查纵向
        result = this.checkVertical();
        if (result) {
            return true;
        }
        
        // 检查斜向（从左上到右下）
        result = this.checkOblique1();
        if (result) {
            return true;
        }

        // 检查斜向（从左下到右上）
        result = this.checkOblique2();
        return result;
    }
    
    /**
     * 登录。
     * @param username  用户名
     */
    public void logon(String username) {
        // 发送登录请求
        final LogonRequest request = new LogonRequest(username);
        this.sendData(request);
    }

    /**
     * 游戏区域的单击响应方法。
     * @param x, y  用户单击的位置
     */
    public void onGameBoardClicked(int x, int y) {
        final int bound = Const.GRID_SIZE / 2;  // 格的尺寸的一半

        // 如果不是己方下棋中（对手下棋中或游戏结束）或者 单击的范围超出棋盘，则直接返回
        if (mStatus != Status.PLAYING || 
            x < bound || x > Const.GAME_BOARD_WIDTH - bound || 
            y < bound || y > Const.GAME_BOARD_HEIGHT - bound) {
            return;
        }
        
        // 将用户单击的点转换为离得最近的棋盘点
        int tempX = x;
        int tempY = y;
        int temp = x % Const.GRID_SIZE;
        if (temp >= bound) {
            tempX += Const.GRID_SIZE - temp;
        } else {
            tempX -= temp;
        }
        
        temp = y % Const.GRID_SIZE;
        if (temp >= bound) {
            tempY += Const.GRID_SIZE - temp;
        } else {
            tempY -= temp;
        }
        
        // 将窗口坐标转换成棋盘坐标
        tempX = tempX / Const.GRID_SIZE - 1;
        tempY = tempY / Const.GRID_SIZE - 1;

        // 放置棋子
        final boolean put = this.putPiece(mIsBlack, tempX, tempY);
        if (!put) {
            return;
        }
        
        // 判断是否胜利
        final boolean won = this.isWon();
        
        // 将已放置的棋子发给对手
        final PutPiece putPiece = new PutPiece(mRivalName, tempX, tempY);
        this.sendData(putPiece);
        
        // 判断是否胜利
        if (won) {
            // 发送游戏结束数据
            GameOver gameOver = new GameOver(GameOverReason.WON, mRivalName);
            this.sendData(gameOver);

            // 游戏结束
            this.gameOver(GameOverReason.WON);
        } else {
            mStatus = Status.RIVAL_PLAYING;
        }
    }
    
    /**
     * 处理服务器发来的数据。
     * @param pro   协议
     */
    private void onGetMessageFromServer(GomokuProtocol pro) {
        switch (pro.mType) {
            case FIND_USERS:
                this.handleFindUsers((FindUsersResult)pro);
                break;
            case GAME_OVER:
                this.handleGameOver((GameOver)pro);
                break;
            case INVITE_REQUEST:
                this.handleInviteRequest((InviteRequest)pro);
                break;
            case INVITE_RESULT:
                this.handleInviteResult((InviteResult)pro);
                break;
            case LOGON:
                this.handleLogon((LogonResult)pro);
                break;
            case PUT_PIECE:
                this.handlePutPiece((PutPiece)pro);
                break;
            case QUIT_GAME:
                this.handleQuitGame((QuitGame)pro);
                break;
        }
    }
    
    /**
     * 放置棋子。
     * @param isBlack 黑棋则为 true，白棋则为 false
     * @param x, y    棋盘坐标
     * @return 成功则返回 true，否则返回 false
     */
    boolean putPiece(boolean isBlack, int x, int y) {
        // 如果超出棋盘区域，则返回 false
        if (x >= Const.GAME_BOARD_POINT_COUNT_H || y >= Const.GAME_BOARD_POINT_COUNT_V) {
            return false;
        }
        
        // 如果当前点已经有棋子了，则返回 false
        if (mPieces[y][x] != Piece.NOTHING) {
            return false;
        }
        
        // 画实心圆
        if (isBlack) {
            mDrawer.setColor(Color.BLACK);
        } else {
            mDrawer.setColor(Color.WHITE);
        }
        mDrawer.fillOval((x + 1) * Const.GRID_SIZE - Const.PIECE_RADIUS, 
            (y + 1) * Const.GRID_SIZE - Const.PIECE_RADIUS, Const.PIECE_RADIUS * 2, Const.PIECE_RADIUS * 2);
        
        // 通知游戏区域重绘
        mGameBoard.repaint();
        
        // 保存最后放置棋子的位置
        mLastPos.x = x;
        mLastPos.y = y;
        
        // 保存到所有棋子中
        if (isBlack) {
            mPieces[y][x] = Piece.BLACK;
        } else {
            mPieces[y][x] = Piece.WHITE;
        }

        return true;
    }
    
    /**
     * 游戏重新开始。
     */
    public void restart() {
        // 发送挑战请求
        final InviteRequest request = new InviteRequest(mUsername, mRivalName);
        this.sendData(request);
    }

    /**
     * 接收数据的线程方法。
     */
    @Override
    public void run() {
        ObjectInputStream receiver = null;  // 数据接收者

        try {
            // 创建数据接收者
            receiver = new ObjectInputStream(super.getInputStream());

            // 循环接收数据，直到自己被关闭
            while (!super.isClosed()) {
                // 接收数据并转换成协议
                final GomokuProtocol pro = (GomokuProtocol)receiver.readObject();
                
                // 处理协议
                this.onGetMessageFromServer(pro);
            }

            receiver.close();
        } catch (Exception e) {
            try {
                assert receiver != null;
                receiver.close();
            } catch (Exception e2) {
                e2.printStackTrace();
            }
        }
    }
    
    /**
     * 发送数据。
     * @param pro   协议
     */
    public void sendData(GomokuProtocol pro) {
        try {
            mSender.writeObject(pro);

            // 防止发送过快导致服务器无法响应
            Thread.sleep(100);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    /**
     * 游戏开始。
     * @param isBlack   黑棋则为 true，白棋则为 false
     */
    public void start(boolean isBlack) {
        // 重绘游戏区域
        this.drawGameBoard();

        // 清空所有棋子
        mPieces = new Piece[Const.GAME_BOARD_POINT_COUNT_V][Const.GAME_BOARD_POINT_COUNT_H];
        for (int i = 0; i < Const.GAME_BOARD_POINT_COUNT_H; i++) {
            for (int j = 0; j < Const.GAME_BOARD_POINT_COUNT_V; j++) {
                mPieces[i][j] = Piece.NOTHING;
            }
        }

        // 设置成员变量
        mStatus = isBlack ? Status.PLAYING: Status.RIVAL_PLAYING;
        mIsBlack = isBlack;
        
        // 发送通知
        mCallback.onGameStart(mRivalName);
    }
}
